/*
 * Copyright 2015 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.javascript.jscomp;

import com.google.common.collect.ImmutableMap;
import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
import com.google.javascript.jscomp.testing.TestExternsBuilder;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class Es6RewriteArrowFunctionTest extends CompilerTestCase {

  private LanguageMode languageOut;

  public Es6RewriteArrowFunctionTest() {
    super(MINIMAL_EXTERNS);
  }

  @Override
  @Before
  public void setUp() throws Exception {
    super.setUp();

    setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015);
    languageOut = LanguageMode.ECMASCRIPT3;

    enableNormalize();
    enableTypeInfoValidation();
    enableTypeCheck();
    replaceTypesWithColors();
    enableMultistageCompilation();
    setGenericNameReplacements(
        ImmutableMap.of(
            "$jscomp$this$UID", "$jscomp$this$", "$jscomp$arguments$UID", "$jscomp$arguments$"));
  }

  @Override
  protected CompilerOptions getOptions() {
    CompilerOptions options = super.getOptions();
    options.setLanguageOut(languageOut);
    return options;
  }

  @Override
  protected Es6RewriteArrowFunction getProcessor(Compiler compiler) {
    return new Es6RewriteArrowFunction(compiler);
  }

  @Test
  public void testAssigningArrowToVariable_BlockBody() {
    test("var f = x => { return x+1; };", "var f = function(x) { return x+1; };");
  }

  @Test
  public void testAssigningArrowToVariable_ExpressionBody() {
    test("var f = x => x+1;", "var f = function(x) { return x+1; };");
  }

  @Test
  public void testPassingArrowToMethod_ExpressionBody() {
    test(
        externs(new TestExternsBuilder().addArray().build()),
        srcs("var odds = [1,2,3,4].filter((n) => n%2 == 1);"),
        expected("var odds = [1,2,3,4].filter(function(n) { return n%2 == 1; });"));
  }

  @Test
  public void testCapturingThisInArrow_ExpressionBody() {
    test(
        "var f = () => this;",
        "const $jscomp$this$UID$0 = this; var f = function() { return $jscomp$this$UID$0; };");
  }

  @Test
  public void testCapturingThisInArrow_BlockBody() {
    test(
        externs(
            """
            window.init = function() { };
            window.doThings = function() { };
            window.done = function() { };
            """),
        srcs(
            """
            var f = x => {
              this.init();
              this.doThings();
              this.done();
            };
            """),
        expected(
            """
            const $jscomp$this$UID$0 = this;
            var f = function(x) {
              $jscomp$this$UID$0.init();
              $jscomp$this$UID$0.doThings();
              $jscomp$this$UID$0.done();
            };
            """));
  }

  @Test
  public void testCapturingThisInArrowPlacesAliasAboveContainingStatement() {
    // We use `switch` here because it's a very complex kind of statement.
    test(
        "switch(a) { case b: (() => { this; })(); }",
        """
        const $jscomp$this$UID$0 = this;
        switch(a) {
          case b:
            (function() { $jscomp$this$UID$0; })();
        }
        """);
  }

  @Test
  public void testCapturingThisInMultipleArrowsPlacesOneAliasAboveContainingStatement() {
    // We use `switch` here because it's a very complex kind of statement.
    test(
        """
        switch(a) {
          case b:
            (() => { this; })();
          case c:
            (() => { this; })();
        }
        """,
        """
        const $jscomp$this$UID$0 = this;
        switch(a) {
          case b:
            (function() { $jscomp$this$UID$0; })();
          case c:
            (function() { $jscomp$this$UID$0; })();
        }
        """);
  }

  @Test
  public void testCapturingThisInMultipleArrowsPlacesOneAliasAboveAllContainingStatements() {
    // We use `switch` here because it's a very complex kind of statement.
    test(
        """
        switch(a) {
          case b:
            (() => { this; })();
        }
        switch (c) {
          case d:
            (() => { this; })();
        }
        """,
        """
        const $jscomp$this$UID$0 = this;
        switch(a) {
          case b:
            (function() { $jscomp$this$UID$0; })();
        }
        switch (c) {
          case d:
            (function() { $jscomp$this$UID$0; })();
        }
        """);
  }

  @Test
  public void testCapturingEnclosingFunctionArgumentsInArrow() {
    test(
        """
        function f() {
          var x = () => arguments;
        }
        """,
        """
        function f() {
          const $jscomp$arguments$UID$1 = arguments;
          var x = function() { return $jscomp$arguments$UID$1; };
        }
        """);
  }

  @Test
  public void testAssigningArrowToObjectLiteralField_ExpressionBody() {
    test("var obj = { f: () => 'bar' };", "var obj = { f: function() { return 'bar'; } };");
  }

  @Test
  public void testCapturingThisInArrowFromClassMethod() {
    // TODO(b/76024335): Enable these validations and checks.
    // We need to test classes the type-checker doesn't understand class syntax and fails before the
    // test even runs.
    disableTypeInfoValidation();
    disableTypeCheck();

    test(
        """
        class C {
          constructor() {
            this.counter = 0;
          }

          init() {
            document.onclick = () => this.logClick();
          }

          logClick() {
             this.counter++;
          }
        }
        """,
        """
        class C {
          constructor() {
            this.counter = 0;
          }

          init() {
            const $jscomp$this$UID$2 = this;
            document.onclick = function() {return $jscomp$this$UID$2.logClick()}
          }

          logClick() {
             this.counter++;
          }
        }
        """);
  }

  @Test
  public void testCapturingThisInArrowFromClassConstructorWithSuperCall() {
    // TODO(b/76024335): Enable these validations and checks.
    // We need to test super, but super only makes sense in the context of a class, but
    // the type-checker doesn't understand class syntax and fails before the test even runs.
    disableTypeInfoValidation();
    disableTypeCheck();

    test(
        """
        class B {
          constructor(x) {
            this.x = x;
          }
        }
        class C extends B {
          constructor(x, y) {
            console.log('statement before super');
            super(x);
            this.wrappedXGetter = () => this.x;
            this.y = y;
            this.wrappedYGetter = () => this.y;
          }
        }
        """,
        """
        class B {
          constructor(x) {
            this.x = x;
          }
        }
        class C extends B {
          constructor(x$jscomp$1, y) {
            console.log('statement before super');
            super(x$jscomp$1);
            const $jscomp$this$UID$2 = this; // Must not use `this` before super() call.
            this.wrappedXGetter = function() { return $jscomp$this$UID$2.x; };
            this.y = y;
            this.wrappedYGetter = function() { return $jscomp$this$UID$2.y; };
          }
        }
        """);
  }

  @Test
  public void testCapturingThisInArrowFromClassConstructorWithMultipleSuperCallPaths() {
    // TODO(b/76024335): Enable these validations and checks.
    // We need to test super, but super only makes sense in the context of a class, but
    // the type-checker doesn't understand class syntax and fails before the test even runs.
    disableTypeInfoValidation();
    disableTypeCheck();

    test(
        """
        class B {
          constructor(x) {
            this.x = x;
          }
        }
        class C extends B {
          constructor(x, y) {
            if (x < 1) {
              super(x);
            } else {
              super(-x);
            }
            this.wrappedXGetter = () => this.x;
            this.y = y;
            this.wrappedYGetter = () => this.y;
          }
        }
        """,
        """
        class B {
          constructor(x) {
            this.x = x;
          }
        }
        class C extends B {
          constructor(x$jscomp$1, y) {
            if (x$jscomp$1 < 1) {
              super(x$jscomp$1);
            } else {
              super(-x$jscomp$1);
            }
            const $jscomp$this$UID$2 = this; // Must not use `this` before super() call.
            this.wrappedXGetter = function() { return $jscomp$this$UID$2.x; };
            this.y = y;
            this.wrappedYGetter = function() { return $jscomp$this$UID$2.y; };
          }
        }
        """);
  }

  @Test
  public void testMultipleArrowsInSameFreeScope() {
    test(
        "var a1 = x => x+1; var a2 = x => x-1;",
        """
        var a1 = function(x) { return x+1; };
        var a2 = function(x$jscomp$1) { return x$jscomp$1-1; };
        """);
  }

  @Test
  public void testMultipleArrowsInSameFunctionScope() {
    test(
        "function f() { var a1 = x => x+1; var a2 = x => x-1; }",
        """
        function f() {
          var a1 = function(x) { return x+1; };
          var a2 = function(x$jscomp$1) { return x$jscomp$1-1; };
        }
        """);
  }

  @Test
  public void testCapturingThisInMultipleArrowsInSameFunctionScope() {
    test(
        """
        ({
          x: 0,
          y: 'a',
          f: function() {
            var a1 = () => this.x;
            var a2 = () => this.y;
          },
        })
        """,
        """
        ({
          x: 0,
          y: 'a',
          f: function() {
            const $jscomp$this$UID$1 = this;
            var a1 = function() { return $jscomp$this$UID$1.x; };
            var a2 = function() { return $jscomp$this$UID$1.y; };
          },
        })
        """);
  }

  @Test
  public void testGeneratedVariableDeclarations_placedAfterFunctionDeclarations() {
    test(
        """
        ({
          x: 0,
          y: 'a',
          f: function() {
            function foo() { this.x;}
            var a2 = () => this.y;
          },
        })
        """,
        """
        ({
          x: 0,
          y: 'a',
          f: function() {
        // stays hoisted
            function foo() { this.x; }
        // variable declarations placed after function declarations
            const $jscomp$this$UID$1 = this;
            var a2 = function() { return $jscomp$this$UID$1.y; };
          },
        })
        """);
  }

  @Test
  public void testPassingMultipleArrowsInSameFreeScopeAsMethodParams() {
    test(
        externs(
            MINIMAL_EXTERNS
                + """
                /**
                 * @template Y
                 * @param {function(T):Y} mapper
                 * @return {!Array<Y>}
                 */
                Array.prototype.map = function(mapper) { };
                """),
        srcs("var a = [1,2,3,4]; var b = a.map(x => x+1).map(x => x*x);"),
        expected(
"""
var a = [1,2,3,4];
var b = a.map(function(x) { return x+1; }).map(function(x$jscomp$1) { return x$jscomp$1*x$jscomp$1; });
"""));
  }

  @Test
  public void testMultipleArrowsInSameFunctionScopeAsMethodParams() {
    test(
        externs(
            MINIMAL_EXTERNS
                + """
                /**
                 * @template Y
                 * @param {function(T):Y} mapper
                 * @return {!Array<Y>}
                 */
                Array.prototype.map = function(mapper) { };
                """),
        srcs(
            """
            function f() {
              var a = [1,2,3,4];
              var b = a.map(x => x+1).map(x => x*x);
            }
            """),
        expected(
"""
function f() {
  var a = [1,2,3,4];
  var b = a.map(function(x) { return x+1; }).map(function(x$jscomp$1) { return x$jscomp$1*x$jscomp$1; });
}
"""));
  }

  @Test
  public void testCapturingThisInArrowFromNestedScopes() {
    test(
        """
        var outer = {
          x: null,

          f: function() {
             var a1 = () => this.x;
             var inner = {
               y: null,

               f: function() {
                 var a2 = () => this.y;
               }
             };
          }
        }
        """,
        """
        var outer = {
          x: null,

          f: function() {
             const $jscomp$this$UID$1 = this;
             var a1 = function() { return $jscomp$this$UID$1.x; }
             var inner = {
               y: null,

               f: function() {
                 const $jscomp$this$UID$2 = this;
                 var a2 = function() { return $jscomp$this$UID$2.y; }
               }
             };
          }
        }
        """);
  }

  @Test
  public void testCapturingThisInArrowWithNestedConstructor() {
    test(
        """
        ({
          f: null,

          g: function() {
            var setup = () => {
              /** @constructor @struct */
              function Foo() { this.x = 5; }

              this.f = new Foo;
            };
          },
        })
        """,
        """
        ({
          f: null,

          g: function() {
            const $jscomp$this$UID$1 = this;
            var setup = function() {
              /** @constructor */
              function Foo() { this.x = 5; }

              $jscomp$this$UID$1.f = new Foo;
            };
          },
        })
        """);
  }

  @Test
  public void testNestingArrow() {
    test(
        externs(""),
        srcs("var f = x =>\n y => x+y;"),
        expected("var f = function(x) {return function(y) { return x+y; }; };"));
  }

  @Test
  public void testNestingArrowsCapturingThis() {
    test(
        externs("window.foo = function() { };"),
        srcs("var f = (x => { var g = (y => { this.foo(); }) });"),
        expected(
            """
            const $jscomp$this$UID$0 = this;
            var f = function(x) {
              var g = function(y) {
                $jscomp$this$UID$0.foo();
              }
            }
            """));
  }
}
